{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Chapter 1\n",
    "\n",
    "# Vectors, Matrices, and Arrays\n",
    "\n",
    "### 1.0 Introduction\n",
    "Numpy is the foundation of the Python machine learning stack. It allows for efficient operations on the data structures often used in machine learning: vectors, matricies, and tensors.\n",
    "\n",
    "This chapter covers the most common NumPy operations we are likely to run into\n",
    "\n",
    "### 1.1 Creating a Vector\n",
    "#### Problem\n",
    "You need to create a vector\n",
    "#### Solution\n",
    "Use Numpy to create a one-dimensional array"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create a vector as a row\n",
    "vector_row = np.array([1, 2, 3])\n",
    "\n",
    "# create a vetor as a column\n",
    "vector_column = np.array([[1],\n",
    "                          [2],\n",
    "                          [3]])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "Numpy's main data structure is the multidimensional array\n",
    "\n",
    "#### See Also\n",
    "* Vectors, Math is Fun (https://www.mathsisfun.com/algebra/vectors.html)\n",
    "* Euclidian vector, Wikipedia (https://en.wikipedia.org/wiki/Euclidean_vector)\n",
    "\n",
    "### 1.2 Creating a Matrix\n",
    "\n",
    "#### Problem\n",
    "You need to create a matrix.\n",
    "\n",
    "#### Solution\n",
    "Use Numpy to create a two-dimensional array:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create a matrix\n",
    "matrix = np.array([[1, 2],\n",
    "                   [1, 2],\n",
    "                   [1, 2]])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "To create a matrix we can use a NumPy two-dimensional array. In our solution, the matrix contains three rows and two columns (a column of 1s and a column of 2s)\n",
    "\n",
    "NumPy actually has a dedicated matrix data structure:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "matrix_object = np.mat([[1, 2],\n",
    "                        [1, 2],\n",
    "                        [1, 2]])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "However the matrix data structure is not recommended for two reaons. First, arrays are the de facto standard data structure of NumPy. Second the vast majority of NumPy operations return arrays, not matrix objects.\n",
    "\n",
    "#### See Also\n",
    "* Matrix, Wikipedia (https://en.wikipedia.org/wiki/Matrix_(mathematics)\n",
    "* Matrix, Wolfram MathWorld (http://mathworld.wolfram.com/Matrix.html)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.3 Creating a Sparse Matrix\n",
    "#### Problem\n",
    "Given data with very few nonzero values, you want to efficiently represent it.\n",
    "\n",
    "#### Solution\n",
    "Create a sparse matrix:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# load libraries\n",
    "import numpy as np\n",
    "from scipy import sparse\n",
    "\n",
    "# create a matrix\n",
    "matrix = np.array([[0, 0],\n",
    "                  [0, 1],\n",
    "                  [3, 0]])\n",
    "\n",
    "# create compressed sparse row (CSR) matrix\n",
    "matrix_sparse = sparse.csr_matrix(matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "#### Discussion\n",
    "A frequent situation in machine learning is having a huge amount of data; however most of the elements in the data are zeros. For example, imagine a matrix where the columns are every movie on Netflix, the rows are every Netflix user, and the values are how many times a user has watched that particular movie. This matrix would have tens of thousands of columns and millions of rows! However, since most users do not watch most movies, the vast majority of elements would be zero.\n",
    "\n",
    "Sparse matricies only store nonzero elements and assume all other values will be zero, leading to significant computational savings. In our solution, we created a Numpy array with two nonzero values, then converted it into a sparse matrix. If we view the sparse matrix we can see that only the nonzero values are stored:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  (1, 1)\t1\n",
      "  (2, 0)\t3\n"
     ]
    }
   ],
   "source": [
    "# view sparse matrix\n",
    "print(matrix_sparse)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There are a number of types of sparse matrices. However, in compressed sparse row (CSR) matrices, (1, 1) and (2, 0) represent the (zero-indexed) indices of the non-zero values 1 and 3, respectively. For example, the element 1 is in the second row and second column. We can see the advantage of sparse matrices if we create a much larger matrix with many more zero elements and then compare this larger matrix with our original sparse matrix:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  (1, 1)\t1\n",
      "  (2, 0)\t3\n"
     ]
    }
   ],
   "source": [
    "# create larger matrix\n",
    "matrix_large = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n",
    "                         [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],\n",
    "                         [3, 0, 0, 0, 0, 0, 0, 0, 0, 0]])\n",
    "\n",
    "# create compressed sparse row (CSR) matrix\n",
    "matrix_large_sparse = sparse.csr_matrix(matrix_large)\n",
    "\n",
    "# view original sparse matrix\n",
    "print(matrix_sparse)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  (1, 1)\t1\n",
      "  (2, 0)\t3\n"
     ]
    }
   ],
   "source": [
    "# view larger sparse matrix\n",
    "print(matrix_large_sparse)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As we can see, despite the fact that we added many more zero elements in the larger matrix, its sparse representation is exactly the same as our original sparse matrix. That is, the addition of zero elements did not change the size of the sparse matrix.\n",
    "\n",
    "As mentioned, there are many different types of sparse matrices, such as compressed sparse column, list of lists, and dictionary of keys. While an explanation of the different types and their implications is outside the scope of this book, it is worth noting that while there is no “best” sparse matrix type, there are meaningful differences between them and we should be conscious about why we are choosing one type over another.\n",
    "\n",
    "#### See Also\n",
    "* Sparse matrices, SciPy documentation (https://docs.scipy.org/doc/scipy/reference/sparse.html)\n",
    "* 101 Ways to Store a Sparse Matrix (https://medium.com/@jmaxg3/101-ways-to-store-a-sparse-matrix-c7f2bf15a229)\n",
    "\n",
    "### 1.4 Selected Elements\n",
    "#### Problem\n",
    "You need to select one or more elements in a vector or matrix.\n",
    "\n",
    "#### Solution\n",
    "NumPy's arrays make that easy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create row vector\n",
    "vector = np.array([1, 2, 3, 4, 5, 6])\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [4, 5, 6],\n",
    "                   [7, 8, 9]])\n",
    "\n",
    "# select the third element of vector\n",
    "vector[2]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "5"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# select second row, second column\n",
    "matrix[1,1]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "Like most things in Python, NumPy arrays are zero-indexed, meaning that the index of the first element is 0, not 1. With that caveat, NumPy offers a wide variety of methods for selecting (i.e., indexing and slicing) elements or groups of elements in arrays:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1, 2, 3, 4, 5, 6])"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Select all elements of a vector\n",
    "vector[:]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1, 2, 3])"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# select everything up to and including the third element\n",
    "vector[:3]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "6"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# select the last element\n",
    "vector[-1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1, 2, 3],\n",
       "       [4, 5, 6]])"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# select the first two rows and all columns of a matrix\n",
    "matrix[:2, :]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[2],\n",
       "       [5],\n",
       "       [8]])"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# select all rows and the second column\n",
    "matrix[:,1:2]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.5 Describing a Matrix\n",
    "#### Problem\n",
    "You want to describe the shape, size, and dimensions of the matrix\n",
    "\n",
    "#### Solution\n",
    "Use shape, size, and ndim:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(3, 4)"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 2, 3, 4],\n",
    "                   [5, 6, 7, 8],\n",
    "                   [9, 10, 11, 12]])\n",
    "\n",
    "# view number of rows and columns\n",
    "matrix.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "12"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# view number of elements (rows * columns)\n",
    "matrix.size"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# view number of dimensions\n",
    "matrix.ndim"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "This might seem basic (and it is); however, time and again it will be valuable to check the shape and size of an array both for further calculations and simply as a gut check after some operation\n",
    "\n",
    "### 1.6 Applying Operations to Elements\n",
    "#### Problem\n",
    "You want to apply some function to multiple elements in an array.\n",
    "\n",
    "#### Solutions\n",
    "Use NumPy's vectorize:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1001, 1002, 1003],\n",
       "       [1004, 1005, 1006],\n",
       "       [1007, 1008, 1009]])"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [4, 5, 6],\n",
    "                   [7, 8, 9]])\n",
    "\n",
    "# create function that adds 1000 to something\n",
    "add_1000 = lambda i: i + 1000\n",
    "\n",
    "# create vectorized function\n",
    "vectorized_add_1000 = np.vectorize(add_1000)\n",
    "\n",
    "# apply function to all elementsin matrix\n",
    "vectorized_add_1000(matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discusion\n",
    "NumPy’s vectorize class converts a function into a function that can apply to all elements in an array or slice of an array. It’s worth noting that vectorize is essentially a for loop over the elements and does not increase performance. Furthermore, NumPy arrays allow us to perform operations between arrays even if their dimensions are not the same (a process called broadcasting). For example, we can create a much simpler version of our solution using broadcasting:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1001, 1002, 1003],\n",
       "       [1004, 1005, 1006],\n",
       "       [1007, 1008, 1009]])"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# add 1000 to all elements\n",
    "matrix + 1000"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Finding Maximum and Minimum Values\n",
    "#### Problem\n",
    "You need to find the maximum or minimum value in an array.\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's max and min:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "9"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [4, 5, 6],\n",
    "                   [7, 8, 9]])\n",
    "\n",
    "# rreturn maximum element\n",
    "np.max(matrix)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# return minimum element\n",
    "np.min(matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "Often we want to know the maximum and minimum value in an array or subset of an array. This can be accomplished with the max and min methods. Using the axis parameter we can also apply the operation along a certain axis:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([7, 8, 9])"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# find maximum element in each column\n",
    "np.max(matrix, axis=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([3, 6, 9])"
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# find maximum element in each row\n",
    "np.max(matrix, axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.8 Calculating the Average, Variance, and Standard Deviation\n",
    "\n",
    "#### Problem\n",
    "You want to calculate some descriptive statistics about an array.\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's mean, var, and std:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "5.0"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [4, 5, 6],\n",
    "                   [7, 8, 9]])\n",
    "\n",
    "# return mean\n",
    "np.mean(matrix)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "6.666666666666667"
      ]
     },
     "execution_count": 34,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# return variance\n",
    "np.var(matrix)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2.581988897471611"
      ]
     },
     "execution_count": 35,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# return standard deviation\n",
    "np.std(matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "Just like with max and min, we can easily get descriptive statistics about the whole matrix or do calculations alon a single axis:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([4., 5., 6.])"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# find the mean value in each column\n",
    "np.mean(matrix, axis=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.9 Reshaping Arrays\n",
    "#### Problem\n",
    "You want to change the shape (number of rows and columns) of an array without changing the element values.\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's reshape:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 1,  2,  3,  4,  5,  6],\n",
       "       [ 7,  8,  9, 10, 11, 12]])"
      ]
     },
     "execution_count": 37,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create 4x3 matrix\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [4, 5, 6],\n",
    "                   [7, 8, 9],\n",
    "                   [10, 11, 12]])\n",
    "\n",
    "# reshape matrix into 2x6 matrix\n",
    "matrix.reshape(2, 6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "reshape allows us to restructure an array so that we maintain the same data but it is organized as a different number of rows and columns. The only requirement is that the shape of the original and new matrix contain the same number of elements (i.e., the same size). We can see the size of a matrix using size:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "12"
      ]
     },
     "execution_count": 38,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "matrix.size"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One useful argument in reshape is -1, which effectively means “as many as needed,” so reshape(-1, 1) means one row and as many columns as needed:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12]])"
      ]
     },
     "execution_count": 39,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "matrix.reshape(1, -1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, if we provide one integer, reshape will return a 1D array of that length:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12])"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "matrix.reshape(12)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.10 Transposing a Vector or Matrix\n",
    "#### Problem\n",
    "You need to transpose a vector or matrix\n",
    "\n",
    "#### Solution\n",
    "Use the T method:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1, 4, 7],\n",
       "       [2, 5, 8],\n",
       "       [3, 6, 9]])"
      ]
     },
     "execution_count": 41,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [4, 5, 6],\n",
    "                   [7, 8, 9]])\n",
    "\n",
    "# transpose matrix\n",
    "matrix.T"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Transposing is a common operation in linear algebra where the column and row indices of each element are swapped. One nuanced point that is typically overlooked outside of a linear algebra class is that, technically, a vector cannot be transposed because it is just a collection of values:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1, 2, 3, 4, 5, 6])"
      ]
     },
     "execution_count": 42,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# transpose vector\n",
    "np.array([1, 2, 3, 4, 5, 6]).T"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "However, it is common to refer to transposing a vector as converting a row vector to a column vector (notice the second pair of brackets) or vice versa:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1],\n",
       "       [2],\n",
       "       [3],\n",
       "       [4],\n",
       "       [5],\n",
       "       [6]])"
      ]
     },
     "execution_count": 43,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# transpose row vector\n",
    "np.array([[1, 2, 3, 4, 5, 6]]).T"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.11 Flattening a Matrix\n",
    "#### Problem\n",
    "You need to transform a matrix into a one-dimensional array.\n",
    "\n",
    "#### Solution\n",
    "Use flatten:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1, 2, 3, 4, 5, 6, 7, 8, 9])"
      ]
     },
     "execution_count": 44,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [4, 5, 6],\n",
    "                   [7, 8, 9]])\n",
    "\n",
    "# flatten matrix\n",
    "matrix.flatten()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "flatten is a simple method to transform a matrix into a one-dimensional array. Alternatively, we can use reshape to create a row vector:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1, 2, 3, 4, 5, 6, 7, 8, 9]])"
      ]
     },
     "execution_count": 45,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "matrix.reshape(1, -1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.12 Finding the Rank of a Matrix\n",
    "#### Problem\n",
    "You need to know the rank of a matrix\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's linear algebra method matrix_rank:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2"
      ]
     },
     "execution_count": 46,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 1, 1],\n",
    "                   [1, 1, 10],\n",
    "                   [1, 1, 15]])\n",
    "\n",
    "# return matrix rank\n",
    "np.linalg.matrix_rank(matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "The rank of a matrix is the dimensions of the vector space spanned by its columns or rows. Finding the rank of a matrix is easy in NumPy thanks to matrix_rank."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### See Also\n",
    "* The Rank of a Matrix, CliffsNotes (https://www.cliffsnotes.com/study-guides/algebra/linear-algebra/real-euclidean-vector-spaces/the-rank-of-a-matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.13 Calculating the Determinant\n",
    "#### Problem\n",
    "You need to know the determinant of a matrix\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's linear algebra method det:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.0"
      ]
     },
     "execution_count": 48,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [2, 4, 6],\n",
    "                   [3, 8, 9]])\n",
    "\n",
    "# return the determinant of matrix\n",
    "np.linalg.det(matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "It can sometimes be useful to calculate the determinant of a matrix. NumPy makes this easy with det\n",
    "\n",
    "#### See Also\n",
    "* The determinant | Essence of linear algebra, chapter 5, 3Blue1Brown (https://www.youtube.com/watch?v=Ip3X9LOh2dk)\n",
    "* Determinant, Wolfram MathWorld (http://mathworld.wolfram.com/Determinant.html)\n",
    "\n",
    "### 1.14 Getting the Diagonal of a Matrix\n",
    "#### Problem\n",
    "You need to get the diagonal elements of matrix.\n",
    "\n",
    "#### Solution\n",
    "Use diagonal:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1, 4, 9])"
      ]
     },
     "execution_count": 49,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [2, 4, 6],\n",
    "                   [3, 8, 9]])\n",
    "\n",
    "# return diagonal elements\n",
    "matrix.diagonal()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "NumPy makes getting the diagonal elements of a matrix easy with diagonal. It is also possible to get a diagonal off from the main diagonal by using the offset parameter:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([2, 6])"
      ]
     },
     "execution_count": 50,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# return diagonal one above the main diagonal\n",
    "matrix.diagonal(offset=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([2, 8])"
      ]
     },
     "execution_count": 52,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# return diagonal one below the main diagonal\n",
    "matrix.diagonal(offset=-1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.15 Calculating the Trace of a Matrix\n",
    "#### Problem\n",
    "You need to calculate the trace of a matrix\n",
    "\n",
    "#### Solution\n",
    "Use trace:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "14"
      ]
     },
     "execution_count": 53,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 2, 3],\n",
    "                   [2, 4, 6],\n",
    "                   [3, 8, 9]])\n",
    "\n",
    "# return trace\n",
    "matrix.trace()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "The trace of a matrix is the sum of the diagonal elements and is often used under the hood in machine learning methods. Given a NumPy multidimensional array, we can calculate the trace using trace. We can also return the diagonal of a matrix and calculate its sum:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "14"
      ]
     },
     "execution_count": 54,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# return diagonal and sum elements\n",
    "sum(matrix.diagonal())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### See Also\n",
    "* The Trace of a Square Matrix (http://mathonline.wikidot.com/the-trace-of-a-square-matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.16 Finding Eigenvalues and Eigenvectors\n",
    "#### Problem\n",
    "You need to find the eigenvalues and eigenvectors of a square matrix.\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's linalg.eig:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([13.55075847,  0.74003145, -3.29078992])"
      ]
     },
     "execution_count": 56,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, -1, 3],\n",
    "                   [1, 1, 6],\n",
    "                   [3, 8, 9]])\n",
    "\n",
    "# calculate eigenvalues and eigenvectors\n",
    "eigenvalues, eigenvectors = np.linalg.eig(matrix)\n",
    "\n",
    "# view eigenvalues\n",
    "eigenvalues"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[-0.17622017, -0.96677403, -0.53373322],\n",
       "       [-0.435951  ,  0.2053623 , -0.64324848],\n",
       "       [-0.88254925,  0.15223105,  0.54896288]])"
      ]
     },
     "execution_count": 57,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# view eigenvectors\n",
    "eigenvectors"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "\n",
    "Eigenvectors are widely used in machine learning libraries. Intuitively, given a linear transformation represented by a matrix, $A$, eigenvectors are vectors that, when that transformation is applied, change only in scale (not direction). More formally:\n",
    "\n",
    "$$A v = λ v$$\n",
    "\n",
    "where $A$ is a square matrix, $λ$ contains the eigenvalues and $v$ contains the eigenvectors. In NumPy’s linear algebra toolset, ```eig``` lets us calculate the eigenvalues, and eigenvectors of any square matrix.\n",
    "\n",
    "#### See Also\n",
    "* Eigenvectors and Eigenvalues Explained Visually, Setosa.io (http://setosa.io/ev/eigenvectors-and-eigenvalues/)\n",
    "* Eigenvectors and eigenvalues | Essence of linear algebra, Chapter 10, 3Blue1Brown (https://www.youtube.com/watch?v=PFDu9oVAE-g)\n",
    "\n",
    "### 1.17 Calculating Dot Products\n",
    "#### Problem\n",
    "You need to calculate the dot product of two vectors.\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's dot:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "32"
      ]
     },
     "execution_count": 60,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create two vectors\n",
    "vector_a = np.array([1, 2, 3])\n",
    "vector_b = np.array([4, 5, 6])\n",
    "\n",
    "# calculate dot product\n",
    "np.dot(vector_a, vector_b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "The dot product of two vectors, a and b, is defined as:\n",
    "\n",
    "$$\\sum(a_i * b_i)$$\n",
    "\n",
    "where $a_i$ is the ith element of vector a. We can use NumPy’s dot class to calculate the dot product. Alternatively, in Python 3.5+ we can use the new ```@``` operator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "32"
      ]
     },
     "execution_count": 61,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# calculate dot product\n",
    "vector_a @ vector_b"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### See Also\n",
    "* Vector dot product and vector length, Khan Academy (https://www.khanacademy.org/math/linear-algebra/vectors-and-spaces/dot-cross-products/v/vector-dot-product-and-vector-length)\n",
    "* Dot Product, Paul’s Online Math Notes (http://tutorial.math.lamar.edu/Classes/CalcII/DotProduct.aspx)\n",
    "\n",
    "### 1.18 Adding and Subtracting Matricies\n",
    "#### Problem\n",
    "You want to add or subtract two matricies\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's add and subtract:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 76,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 2,  4,  2],\n",
       "       [ 2,  4,  2],\n",
       "       [ 2,  4, 10]])"
      ]
     },
     "execution_count": 76,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matricies\n",
    "matrix_a = np.array([[1, 1, 1],\n",
    "                     [1, 1, 1],\n",
    "                     [1, 1, 2]])\n",
    "\n",
    "matrix_b = np.array([[1, 3, 1],\n",
    "                     [1, 3, 1],\n",
    "                     [1, 3, 8]])\n",
    "\n",
    "# add two matricies\n",
    "np.add(matrix_a, matrix_b)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 77,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 0, -2,  0],\n",
       "       [ 0, -2,  0],\n",
       "       [ 0, -2, -6]])"
      ]
     },
     "execution_count": 77,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# subtract two matrices\n",
    "np.subtract(matrix_a, matrix_b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "Alternatively, we can simply use the + and - operators:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 2,  4,  2],\n",
       "       [ 2,  4,  2],\n",
       "       [ 2,  4, 10]])"
      ]
     },
     "execution_count": 78,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# add two matricies\n",
    "matrix_a + matrix_b"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.19 Multiplying Matricies\n",
    "#### Problem\n",
    "You want to multiply two matrices.\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's dot:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 79,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[2, 5],\n",
       "       [3, 7]])"
      ]
     },
     "execution_count": 79,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrices\n",
    "matrix_a = np.array([[1, 1],\n",
    "                     [1, 2]])\n",
    "\n",
    "matrix_b = np.array([[1, 3],\n",
    "                     [1, 2]])\n",
    "\n",
    "# multiply two matrices\n",
    "np.dot(matrix_a, matrix_b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "Alternatively, in Python 3.5+ we can use the @ operator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 80,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[2, 5],\n",
       "       [3, 7]])"
      ]
     },
     "execution_count": 80,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# multiply two matrices\n",
    "matrix_a @ matrix_b"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### See Also\n",
    "* Array vs Matrix Operations, MathWorks (https://www.mathworks.com/help/matlab/matlab_prog/array-vs-matrix-operations.html?requestedDomain=true)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.20 Inverting a Matrix\n",
    "#### Problem\n",
    "You want to calculate the inverse of a square matrix.\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's linear algebra inv method:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[-1.66666667,  1.33333333],\n",
       "       [ 0.66666667, -0.33333333]])"
      ]
     },
     "execution_count": 64,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# create matrix\n",
    "matrix = np.array([[1, 4],\n",
    "                  [2, 5]])\n",
    "\n",
    "# calculate inverse of matrix\n",
    "np.linalg.inv(matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "The inverse of a square matrix, $A$, is a second matrix $A^{–1}$, such that:\n",
    "\n",
    "$A * A^{-1} = I$\n",
    "\n",
    "where $I$ is the identity matrix. In NumPy we can use linalg.inv to calculate $A^{–1}$ if it exists. To see this in action, we can multiply a matrix by its inverse and the result is the identity matrix:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1., 0.],\n",
       "       [0., 1.]])"
      ]
     },
     "execution_count": 65,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "matrix @ np.linalg.inv(matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### See Also\n",
    "* Inverse of a Matrix (http://www.mathwords.com/i/inverse_of_a_matrix.htm)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.21 Generating Random Values\n",
    "#### Problem\n",
    "You want to generate pseudorandom values.\n",
    "\n",
    "#### Solution\n",
    "Use NumPy's random:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 69,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0.5488135 , 0.71518937, 0.60276338])"
      ]
     },
     "execution_count": 69,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# load library\n",
    "import numpy as np\n",
    "\n",
    "# set seed\n",
    "np.random.seed(0)\n",
    "\n",
    "# generate three random floats between 0.0 and 1.0\n",
    "np.random.random(3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Discussion\n",
    "NumPy offers a wide variety of means to generate random numbers, many more than can be covered here. In our solution we generated floats; however, it is also common to generate integers:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 70,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([3, 7, 9])"
      ]
     },
     "execution_count": 70,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# genereate three random integers between 1 and 10\n",
    "np.random.randint(0, 11, 3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Alternatively, we can generate numbers by drawing them from a distribution:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 71,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([-1.42232584,  1.52006949, -0.29139398])"
      ]
     },
     "execution_count": 71,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# draw three numbers from a normal distribution with mean 0.0\n",
    "# and standard deviation of 1.0\n",
    "np.random.normal(0.0, 1.0, 3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([-0.98118713, -0.08939902,  1.46416405])"
      ]
     },
     "execution_count": 73,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# draw three numbers from a logistic distribution with mean 0.0 and scale of 1.0\n",
    "np.random.logistic(0.0, 1.0, 3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 75,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1.47997717, 1.3927848 , 1.83607876])"
      ]
     },
     "execution_count": 75,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# draw three numbers greater than or equal to 1.0 and less than 2.0\n",
    "np.random.uniform(1.0, 2.0, 3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, it can sometimes be useful to return the same random numbers multiple times to get predictable, repeatable results. We can do this by setting the “seed” (an integer) of the pseudorandom generator. Random processes with the same seed will always produce the same output. We will use seeds throughout this book so that the code you see in the book and the code you run on your computer produces the same results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:machine_learning_cookbook]",
   "language": "python",
   "name": "conda-env-machine_learning_cookbook-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
